{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "56db45b6",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| default_exp xtend"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "80f4f9d8",
   "metadata": {},
   "source": [
    "# Component extensions\n",
    "> Simple extensions to standard HTML components, such as adding sensible defaults"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8e2d405b",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "from dataclasses import dataclass, asdict\n",
    "from typing import Any\n",
    "\n",
    "from fastcore.utils import *\n",
    "from fastcore.xtras import partial_format\n",
    "from fastcore.xml import *\n",
    "from fastcore.meta import use_kwargs, delegates\n",
    "from fasthtml.core import *\n",
    "from fasthtml.components import *\n",
    "\n",
    "try: from IPython import display\n",
    "except ImportError: display=None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a0319fa2",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pprint import pprint"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "254f4744",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@delegates(ft_hx, keep=True)\n",
    "def A(*c, hx_get=None, target_id=None, hx_swap=None, href='#', **kwargs)->FT:\n",
    "    \"An A tag; `href` defaults to '#' for more concise use with HTMX\"\n",
    "    return ft_hx('a', *c, href=href, hx_get=hx_get, target_id=target_id, hx_swap=hx_swap, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "53efcd61",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<a href=\"#\" ht-get=\"/get\" hx-target=\"#id\">text</a>\n",
       "```"
      ],
      "text/plain": [
       "a(('text',),{'href': '#', 'ht-get': '/get', 'hx-target': '#id'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "A('text', ht_get='/get', target_id='id')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3f0bd0fa",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@delegates(ft_hx, keep=True)\n",
    "def AX(txt, hx_get=None, target_id=None, hx_swap=None, href='#', **kwargs)->FT:\n",
    "    \"An A tag with just one text child, allowing hx_get, target_id, and hx_swap to be positional params\"\n",
    "    return ft_hx('a', txt, href=href, hx_get=hx_get, target_id=target_id, hx_swap=hx_swap, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2531e3fc",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<a href=\"#\" hx-get=\"/get\" hx-target=\"#id\">text</a>\n",
       "```"
      ],
      "text/plain": [
       "a(('text',),{'href': '#', 'hx-get': '/get', 'hx-target': '#id'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "AX('text', '/get', 'id')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "385a6726",
   "metadata": {},
   "source": [
    "## Forms"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "84ea5b04",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@delegates(ft_hx, keep=True)\n",
    "def Form(*c, enctype=\"multipart/form-data\", **kwargs)->FT:\n",
    "    \"A Form tag; identical to plain `ft_hx` version except default `enctype='multipart/form-data'`\"\n",
    "    return ft_hx('form', *c, enctype=enctype, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6aaebe19",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@delegates(ft_hx, keep=True)\n",
    "def Hidden(value:Any=\"\", id:Any=None, **kwargs)->FT:\n",
    "    \"An Input of type 'hidden'\"\n",
    "    return Input(type=\"hidden\", value=value, id=id, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "88863fe3",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@delegates(ft_hx, keep=True)\n",
    "def CheckboxX(checked:bool=False, label=None, value=\"1\", id=None, name=None, **kwargs)->FT:\n",
    "    \"A Checkbox optionally inside a Label, preceded by a `Hidden` with matching name\"\n",
    "    if id and not name: name=id\n",
    "    if not checked: checked=None\n",
    "    res = Input(type=\"checkbox\", id=id, name=name, checked=checked, value=value, **kwargs)\n",
    "    if label: res = Label(res, label)\n",
    "    return Hidden(name=name, skip=True, value=\"\"), res"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3d9e3af6",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<input type=\"hidden\" skip>\n",
       "<label>  <input type=\"checkbox\" checked value=\"1\">\n",
       "Check me out!</label>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "show(CheckboxX(True, 'Check me out!'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "854dc037",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@delegates(ft_html, keep=True)\n",
    "def Script(code:str=\"\", **kwargs)->FT:\n",
    "    \"A Script tag that doesn't escape its code\"\n",
    "    return ft_html('script', NotStr(code), **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d08fe828",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@delegates(ft_html, keep=True)\n",
    "def Style(*c, **kwargs)->FT:\n",
    "    \"A Style tag that doesn't escape its code\"\n",
    "    return ft_html('style', map(NotStr,c), **kwargs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "df6adfeb",
   "metadata": {},
   "source": [
    "## Style and script templates"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c77a0e88",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def double_braces(s):\n",
    "    \"Convert single braces to double braces if next to special chars or newline\"\n",
    "    s = re.sub(r'{(?=[\\s:;\\'\"]|$)', '{{', s)\n",
    "    return re.sub(r'(^|[\\s:;\\'\"])}', r'\\1}}', s)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9ca90ee1",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def undouble_braces(s):\n",
    "    \"Convert double braces to single braces if next to special chars or newline\"\n",
    "    s = re.sub(r'\\{\\{(?=[\\s:;\\'\"]|$)', '{', s)\n",
    "    return re.sub(r'(^|[\\s:;\\'\"])\\}\\}', r'\\1}', s)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "231b7004",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def loose_format(s, **kw):\n",
    "    \"String format `s` using `kw`, without being strict about braces outside of template params\"\n",
    "    if not kw: return s\n",
    "    return undouble_braces(partial_format(double_braces(s), **kw)[0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "29ceb26a",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def ScriptX(fname, src=None, nomodule=None, type=None, _async=None, defer=None,\n",
    "            charset=None, crossorigin=None, integrity=None, **kw):\n",
    "    \"A `script` element with contents read from `fname`\"\n",
    "    s = loose_format(Path(fname).read_text(), **kw)\n",
    "    return Script(s, src=src, nomodule=nomodule, type=type, _async=_async, defer=defer,\n",
    "                  charset=charset, crossorigin=crossorigin, integrity=integrity)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1e72dac9",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def replace_css_vars(css, pre='tpl', **kwargs):\n",
    "    \"Replace `var(--)` CSS variables with `kwargs` if name prefix matches `pre`\"\n",
    "    if not kwargs: return css\n",
    "    def replace_var(m):\n",
    "        var_name = m.group(1).replace('-', '_')\n",
    "        return kwargs.get(var_name, m.group(0))\n",
    "    return re.sub(fr'var\\(--{pre}-([\\w-]+)\\)', replace_var, css)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9bcbff42",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def StyleX(fname, **kw):\n",
    "    \"A `style` element with contents read from `fname` and variables replaced from `kw`\"\n",
    "    s = Path(fname).read_text()\n",
    "    attrs = ['type', 'media', 'scoped', 'title', 'nonce', 'integrity', 'crossorigin']\n",
    "    sty_kw = {k:kw.pop(k) for k in attrs if k in kw}\n",
    "    return Style(replace_css_vars(s, **kw), **sty_kw)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "98f0ea19",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def Nbsp():\n",
    "    \"A non-breaking space\"\n",
    "    return Safe('&nbsp;')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "28fc5f26",
   "metadata": {},
   "source": [
    "## Surreal and JS"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f87a71fd",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def Surreal(code:str):\n",
    "    \"Wrap `code` in `domReadyExecute` and set `m=me()` and `p=me('-')`\"\n",
    "    return Script('''\n",
    "{\n",
    "    const m=me();\n",
    "    const _p = document.currentScript.previousElementSibling;\n",
    "    const p = _p ? me(_p) : null;\n",
    "    domReadyExecute(() => {\n",
    "        %s\n",
    "    });\n",
    "}''' % code)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "74a2220b",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def On(code:str, event:str='click', sel:str='', me=True):\n",
    "    \"An async surreal.js script block event handler for `event` on selector `sel,p`, making available parent `p`, event `ev`, and target `e`\"\n",
    "    func = 'me' if me else 'any'\n",
    "    if sel=='-': sel='p'\n",
    "    elif sel: sel=f'{func}(\"{sel}\", m)'\n",
    "    else: sel='m'\n",
    "    return Surreal('''\n",
    "%s.on(\"%s\", async ev=>{\n",
    "    let e = me(ev);\n",
    "    %s\n",
    "});''' % (sel,event,code))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "17895311",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def Prev(code:str, event:str='click'):\n",
    "    \"An async surreal.js script block event handler for `event` on previous sibling, with same vars as `On`\"\n",
    "    return On(code, event=event, sel='-')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "917c5ad4",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def Now(code:str, sel:str=''):\n",
    "    \"An async surreal.js script block on selector `me(sel)`\"\n",
    "    if sel: sel=f'\"{sel}\"'\n",
    "    return Script('(async (ee = me(%s)) => {\\nlet e = me(ee);\\n%s\\n})()\\n' % (sel,code))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "da93a394",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def AnyNow(sel:str, code:str):\n",
    "    \"An async surreal.js script block on selector `any(sel)`\"\n",
    "    return Script('(async (e = any(\"%s\")) => {\\n%s\\n})()\\n' % (sel,code))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "24d31af2",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def run_js(js, id=None, **kw):\n",
    "    \"Run `js` script, auto-generating `id` based on name of caller if needed, and js-escaping any `kw` params\"\n",
    "    if not id: id = sys._getframe(1).f_code.co_name\n",
    "    kw = {k:dumps(v) for k,v in kw.items()}\n",
    "    return Script(js.format(**kw), id=id, hx_swap_oob='true')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "365f57a8",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def HtmxOn(eventname:str, code:str):\n",
    "    return Script('''domReadyExecute(function() {\n",
    "document.body.addEventListener(\"htmx:%s\", function(event) { %s })\n",
    "})''' % (eventname, code))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "39f20784",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def jsd(org, repo, root, path, prov='gh', typ='script', ver=None, esm=False, **kwargs)->FT:\n",
    "    \"jsdelivr `Script` or CSS `Link` tag, or URL\"\n",
    "    ver = '@'+ver if ver else ''\n",
    "    s = f'https://cdn.jsdelivr.net/{prov}/{org}/{repo}{ver}/{root}/{path}'\n",
    "    if esm: s += '/+esm'\n",
    "    return Script(src=s, **kwargs) if typ=='script' else Link(rel='stylesheet', href=s, **kwargs) if typ=='css' else s"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a32dcd11",
   "metadata": {},
   "source": [
    "## Other helpers"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5e6d3c46",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "class Fragment(FT):\n",
    "    \"An empty tag, used as a container\"\n",
    "    def __init__(self, *c): super().__init__('', c, {}, void_=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "667a0f3f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  <p>1st</p>\n",
      "  <p>2nd</p>\n",
      "\n"
     ]
    }
   ],
   "source": [
    "fts = Fragment(P('1st'), P('2nd'))\n",
    "print(to_xml(fts))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cb0a89d7",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "@delegates(ft_hx, keep=True)\n",
    "def Titled(title:str=\"FastHTML app\", *args, cls=\"container\", **kwargs)->FT:\n",
    "    \"An HTML partial containing a `Title`, and `H1`, and any provided children\"\n",
    "    return Title(title), Main(H1(title), *args, cls=cls, **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "037757de",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<title>my page</title>\n",
       "<main class=\"container\">  <h1>my page</h1>\n",
       "  <p>para</p>\n",
       "</main>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "show(Titled('my page', P('para')))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5ba5cf4e",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def Socials(title, site_name, description, image, url=None, w=1200, h=630, twitter_site=None, creator=None, card='summary'):\n",
    "    \"OG and Twitter social card headers\"\n",
    "    if not url: url=site_name\n",
    "    if not url.startswith('http'): url = f'https://{url}'\n",
    "    if not image.startswith('http'): image = f'{url}{image}'\n",
    "    res = [Meta(property='og:image', content=image),\n",
    "        Meta(property='og:site_name', content=site_name),\n",
    "        Meta(property='og:image:type', content='image/png'),\n",
    "        Meta(property='og:image:width', content=w),\n",
    "        Meta(property='og:image:height', content=h),\n",
    "        Meta(property='og:type', content='website'),\n",
    "        Meta(property='og:url', content=url),\n",
    "        Meta(property='og:title', content=title),\n",
    "        Meta(property='og:description', content=description),\n",
    "        Meta(name='twitter:image', content=image),\n",
    "        Meta(name='twitter:card', content=card),\n",
    "        Meta(name='twitter:title', content=title),\n",
    "        Meta(name='twitter:description', content=description)]\n",
    "    if twitter_site is not None: res.append(Meta(name='twitter:site',    content=twitter_site))\n",
    "    if creator      is not None: res.append(Meta(name='twitter:creator', content=creator))\n",
    "    return tuple(res)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7ff5dc13",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def YouTubeEmbed(video_id:str, *, width:int=560, height:int=315, start_time:int=0, no_controls:bool=False, title:str=\"YouTube video player\", cls:str=\"\", **kwargs):\n",
    "    \"\"\"Embed a YouTube video\"\"\"\n",
    "    if not video_id or not isinstance(video_id, str):\n",
    "        raise ValueError(\"A valid YouTube video ID is required\")\n",
    "    params = []\n",
    "    if start_time>0: params.append(f\"start={start_time}\")\n",
    "    if no_controls: params.append(\"controls=0\")\n",
    "    query_string = \"?\" + \"&\".join(params) if params else \"\"\n",
    "    print(f\"https://www.youtube.com/embed/{video_id}{query_string}\")\n",
    "    return Div(\n",
    "        Iframe(\n",
    "            width=width, height=height,\n",
    "            src=f\"https://www.youtube.com/embed/{video_id}{query_string}\",\n",
    "            title=title, frameborder=\"0\",\n",
    "            allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\",\n",
    "            referrerpolicy=\"strict-origin-when-cross-origin\", allowfullscreen='',\n",
    "            **kwargs\n",
    "        ), cls=cls)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "28c9496d",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def Favicon(light_icon, dark_icon):\n",
    "    \"Light and dark favicon headers\"\n",
    "    return (Link(rel='icon', type='image/x-ico', href=light_icon, media='(prefers-color-scheme: light)'),\n",
    "            Link(rel='icon', type='image/x-ico', href=dark_icon, media='(prefers-color-scheme: dark)'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "50444181",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def clear(id): return Div(hx_swap_oob='innerHTML', id=id)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "09b2e3a5",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "sid_scr = Script('''\n",
    "function uuid() {\n",
    "    return [...crypto.getRandomValues(new Uint8Array(10))].map(b=>b.toString(36)).join('');\n",
    "}\n",
    "\n",
    "sessionStorage.setItem(\"sid\", sessionStorage.getItem(\"sid\") || uuid());\n",
    "\n",
    "htmx.on(\"htmx:configRequest\", (e) => {\n",
    "    const sid = sessionStorage.getItem(\"sid\");\n",
    "    if (sid) {\n",
    "        const url = new URL(e.detail.path, window.location.origin);\n",
    "        url.searchParams.set('sid', sid);\n",
    "        e.detail.path = url.pathname + url.search;\n",
    "    }\n",
    "});\n",
    "''')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "579e1f33",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def with_sid(app, dest, path='/'):\n",
    "    @app.route(path)\n",
    "    def get(): return Div(hx_get=dest, hx_trigger=f'load delay:0.001s', hx_swap='outerHTML')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ca861533",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def LdJson(typ, data:dict, script=False, extra=None, **kwargs)->FT:\n",
    "    \"A script tag containing JSON-LD structured data\"\n",
    "    cts = {'@type':typ, \"@context\": \"https://schema.org\"} | data | (extra or {})\n",
    "    if not script: return cts\n",
    "    return Script(dumps(cts, indent=1), type=\"application/ld+json\", **kwargs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "37b2289c",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def LdContactPoint(contact_type:str, email:str=None, phone:str=None, script=False, **extra)->dict:\n",
    "    \"Create a ContactPoint for JSON-LD\"\n",
    "    data = {\"contactType\": contact_type}\n",
    "    if email: data[\"email\"] = email  \n",
    "    if phone: data[\"telephone\"] = phone\n",
    "    return LdJson(\"ContactPoint\", data, extra=extra, script=script)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c7cd5763",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export  \n",
    "def LdOrg(name:str, url:str=None, logo:str=None, alt_name:str=None, \n",
    "          same_as:list=None, contact_points:list=None, script=False, **extra)->dict:\n",
    "    \"JSON-LD Organization structured data\"\n",
    "    data = {\"name\": name}\n",
    "    if alt_name: data[\"alternateName\"] = alt_name\n",
    "    if url: data[\"url\"] = url\n",
    "    if logo: data[\"logo\"] = logo\n",
    "    if same_as: data[\"sameAs\"] = same_as\n",
    "    if contact_points: data[\"contactPoint\"] = contact_points\n",
    "    return LdJson(\"Organization\", data, extra=extra, script=script)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "27faa056",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<script type=\"application/ld+json\">{\n",
       " \"@type\": \"Organization\",\n",
       " \"@context\": \"https://schema.org\",\n",
       " \"name\": \"AnswerDotAI, Inc.\",\n",
       " \"alternateName\": \"SolveIt\",\n",
       " \"url\": \"https://solve.it.com/\",\n",
       " \"logo\": \"https://solve.it.com/brand/logo.png\",\n",
       " \"sameAs\": [\n",
       "  \"https://www.fast.ai/\",\n",
       "  \"https://twitter.com/fastdotai\"\n",
       " ],\n",
       " \"contactPoint\": [\n",
       "  {\n",
       "   \"@type\": \"ContactPoint\",\n",
       "   \"@context\": \"https://schema.org\",\n",
       "   \"contactType\": \"customer support\",\n",
       "   \"email\": \"support@fast.ai\"\n",
       "  }\n",
       " ]\n",
       "}</script>\n",
       "```"
      ],
      "text/plain": [
       "script(('{\\n \"@type\": \"Organization\",\\n \"@context\": \"https://schema.org\",\\n \"name\": \"AnswerDotAI, Inc.\",\\n \"alternateName\": \"SolveIt\",\\n \"url\": \"https://solve.it.com/\",\\n \"logo\": \"https://solve.it.com/brand/logo.png\",\\n \"sameAs\": [\\n  \"https://www.fast.ai/\",\\n  \"https://twitter.com/fastdotai\"\\n ],\\n \"contactPoint\": [\\n  {\\n   \"@type\": \"ContactPoint\",\\n   \"@context\": \"https://schema.org\",\\n   \"contactType\": \"customer support\",\\n   \"email\": \"support@fast.ai\"\\n  }\\n ]\\n}',),{'type': 'application/ld+json'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "LdOrg(\n",
    "    name=\"AnswerDotAI, Inc.\", alt_name=\"SolveIt\", \n",
    "    url=\"https://solve.it.com/\", logo=\"https://solve.it.com/brand/logo.png\",\n",
    "    same_as=[\"https://www.fast.ai/\", \"https://twitter.com/fastdotai\"],\n",
    "    contact_points=[LdContactPoint(\"customer support\", email=\"support@fast.ai\")],\n",
    "    script=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1af76359",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def LdWebsite(name:str, url:str, script=False, **extra)->dict:\n",
    "    \"Create JSON-LD WebSite structured data\"\n",
    "    data = {\"name\": name, \"url\": url}\n",
    "    return LdJson(\"WebSite\", data, extra=extra, script=script)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "29c7c274",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def LdCourseInstance(course_mode:str=\"Online\", start_date:str=None, end_date:str=None, \n",
    "                     location:dict=None, instructor:dict=None, script=False, **extra)->dict:\n",
    "    \"Create a CourseInstance for JSON-LD\"\n",
    "    data = {\"courseMode\": course_mode}\n",
    "    if start_date: data[\"startDate\"] = start_date\n",
    "    if end_date: data[\"endDate\"] = end_date  \n",
    "    if location: data[\"location\"] = location\n",
    "    if instructor: data[\"instructor\"] = instructor\n",
    "    return LdJson(\"CourseInstance\", data, extra=extra, script=script)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7ca84ba5",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def LdCourse(name:str, description:str, provider:dict, course_instance:dict=None, script=False, **extra)->dict:\n",
    "    \"Create JSON-LD Course structured data\"\n",
    "    data = { \"name\": name, \"description\": description, \"provider\": provider }\n",
    "    if course_instance: data[\"hasCourseInstance\"] = course_instance\n",
    "    return LdJson(\"Course\", data, extra=extra, script=script)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c13a9b1c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "```html\n",
       "<script type=\"application/ld+json\">{\n",
       " \"@type\": \"Course\",\n",
       " \"@context\": \"https://schema.org\",\n",
       " \"name\": \"How to Solve It With Code\",\n",
       " \"description\": \"A 5-week course teaching the SolveIt method...\",\n",
       " \"provider\": {\n",
       "  \"@type\": \"Organization\",\n",
       "  \"name\": \"FastDotAI\",\n",
       "  \"sameAs\": \"https://fast.ai/\"\n",
       " },\n",
       " \"hasCourseInstance\": {\n",
       "  \"@type\": \"CourseInstance\",\n",
       "  \"@context\": \"https://schema.org\",\n",
       "  \"courseMode\": \"Online\",\n",
       "  \"startDate\": \"2025-11-03\",\n",
       "  \"endDate\": \"2025-12-10\",\n",
       "  \"location\": {\n",
       "   \"@type\": \"VirtualLocation\",\n",
       "   \"url\": \"https://solve.it.com/\"\n",
       "  },\n",
       "  \"instructor\": {\n",
       "   \"@type\": \"Person\",\n",
       "   \"name\": \"Jeremy Howard\",\n",
       "   \"sameAs\": \"https://www.fast.ai/about/#jeremy\"\n",
       "  }\n",
       " }\n",
       "}</script>\n",
       "```"
      ],
      "text/plain": [
       "script(('{\\n \"@type\": \"Course\",\\n \"@context\": \"https://schema.org\",\\n \"name\": \"How to Solve It With Code\",\\n \"description\": \"A 5-week course teaching the SolveIt method...\",\\n \"provider\": {\\n  \"@type\": \"Organization\",\\n  \"name\": \"FastDotAI\",\\n  \"sameAs\": \"https://fast.ai/\"\\n },\\n \"hasCourseInstance\": {\\n  \"@type\": \"CourseInstance\",\\n  \"@context\": \"https://schema.org\",\\n  \"courseMode\": \"Online\",\\n  \"startDate\": \"2025-11-03\",\\n  \"endDate\": \"2025-12-10\",\\n  \"location\": {\\n   \"@type\": \"VirtualLocation\",\\n   \"url\": \"https://solve.it.com/\"\\n  },\\n  \"instructor\": {\\n   \"@type\": \"Person\",\\n   \"name\": \"Jeremy Howard\",\\n   \"sameAs\": \"https://www.fast.ai/about/#jeremy\"\\n  }\\n }\\n}',),{'type': 'application/ld+json'})"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "LdCourse(\n",
    "    name=\"How to Solve It With Code\",\n",
    "    description=\"A 5-week course teaching the SolveIt method...\",\n",
    "    provider={\"@type\": \"Organization\", \"name\": \"FastDotAI\", \"sameAs\": \"https://fast.ai/\"},\n",
    "    course_instance=LdCourseInstance(\n",
    "        start_date=\"2025-11-03\", \n",
    "        end_date=\"2025-12-10\",\n",
    "        location={\"@type\": \"VirtualLocation\", \"url\": \"https://solve.it.com/\"},\n",
    "        instructor={\"@type\": \"Person\", \"name\": \"Jeremy Howard\", \"sameAs\": \"https://www.fast.ai/about/#jeremy\"}\n",
    "    ), script=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a3e68a38",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def robots_txt(app, allow_all=True, disallow_paths=None, sitemap_url=None, crawl_delay=None):\n",
    "    \"Add a /robots.txt route to the app\"\n",
    "    @app.route(\"/robots.txt\")\n",
    "    def get():\n",
    "        lines = [\"User-agent: *\"]\n",
    "        if allow_all and not disallow_paths: lines.append(\"Allow: /\")\n",
    "        elif disallow_paths: lines.extend(f\"Disallow: {path}\" for path in disallow_paths)\n",
    "        else: lines.append(\"Disallow: /\")\n",
    "        if crawl_delay: lines.append(f\"Crawl-delay: {crawl_delay}\")\n",
    "        if sitemap_url: lines.append(f\"Sitemap: {sitemap_url}\")\n",
    "        return \"\\n\".join(lines)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b84a4921",
   "metadata": {},
   "source": [
    "Parameters:\n",
    "- `allow_all=True` - Allow all crawling (default)\n",
    "- `disallow_paths` - List of paths to disallow (e.g. `[\"/admin\", \"/private\"]`)\n",
    "- `sitemap_url` - URL to your sitemap.xml\n",
    "- `crawl_delay` - Seconds between requests for polite crawlers\n",
    "\n",
    "Usage examples:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7118aac0",
   "metadata": {},
   "outputs": [],
   "source": [
    "app = FastHTML()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1cc271d9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Allow everything\n",
    "robots_txt(app)\n",
    "\n",
    "# Block specific paths with sitemap\n",
    "robots_txt(app, disallow_paths=[\"/admin\", \"/api\"], sitemap_url=\"https://mysite.com/sitemap.xml\")\n",
    "\n",
    "# Block everything\n",
    "robots_txt(app, allow_all=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1a45d263",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "from fastcore.xml import Url,Loc,Lastmod,Changefreq,Priority,Urlset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fc66d282",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "def sitemap_url(url_info, loc_base=\"\"):\n",
    "    \"Create a sitemap URL element from url_info (string or dict)\"\n",
    "    if isinstance(url_info, str): return Url(Loc(loc_base + url_info))\n",
    "    loc = loc_base + url_info['loc']\n",
    "    url_elem = [Loc(loc)]\n",
    "    if 'lastmod' in url_info: url_elem.append(Lastmod(url_info['lastmod']))\n",
    "    if 'changefreq' in url_info: url_elem.append(Changefreq(url_info['changefreq']))\n",
    "    if 'priority' in url_info: url_elem.append(Priority(str(url_info['priority'])))\n",
    "    return Url(*url_elem)\n",
    "\n",
    "def sitemap_xml(app, urls, loc_base=\"\"):\n",
    "    \"Add a /sitemap.xml route to the app with list of URLs\"\n",
    "    @app.route(\"/sitemap.xml\")\n",
    "    def get():\n",
    "        urlset = [sitemap_url(url_info, loc_base) for url_info in urls]\n",
    "        return Urlset(*urlset, xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "af88d91b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Simple URLs\n",
    "sitemap_xml(app, [\"/\", \"/about\", \"/contact\"], loc_base=\"https://mysite.com\")\n",
    "\n",
    "# With metadata\n",
    "sitemap_xml(app, [\n",
    "    {\"loc\": \"/\", \"changefreq\": \"daily\", \"priority\": 1.0},\n",
    "    {\"loc\": \"/about\", \"changefreq\": \"monthly\", \"priority\": 0.8},\n",
    "    \"/contact\"  # Can mix simple strings with dicts\n",
    "], loc_base=\"https://mysite.com\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "474e14b4",
   "metadata": {},
   "source": [
    "# Export -"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d211e8e2",
   "metadata": {},
   "outputs": [],
   "source": [
    "#| hide\n",
    "import nbdev; nbdev.nbdev_export()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0a942593",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "python3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
